package appml

import (
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"gopkg.in/yaml.v3"
)

// 配置
type Config struct {
	// 本程序的应用名称，如果匹配 Apps 中的名称，则：
	// 可触发本身的自升级；
	// 不会作为被监控拉起的应用。
	Name  string `yaml:"name"`
	Wait  int    `yaml:"wait"`  // 本进程作为升级的辅助进程时，需要等待多长时间（毫秒）后才开始执行升级。
	Multi bool   `yaml:"multi"` // 是否允许多个完全相同的进程同时运行，默认为 false，如果为 false，则启动后新进程会退出
	// 备份目录，用于应用程序升级时的备份。
	// 目录规则：${Backup}/{appname}/{yyyymmddhhmmss}/{Deploy}
	// 备份时，仅简单移动部署目录，不做压缩等额外操作。
	Backup    string            `yaml:"backup"`
	Interval  *ConfigInterval   `yaml:"interval"`  // 间隔配置
	Resources []*ConfigResource `yaml:"resources"` // 资源列表
	Apps      []*ConfigApp      `yaml:"apps"`      // 应用列表
	Uploads   []*ConfigUpload   `yaml:"uploads"`   // 上传列表
}

// 获取应用程序升级时的备份目录
func (c *Config) GetBackupDir(now time.Time, app, deploy string) string {
	return filepath.Join(c.Backup, app, now.Format("20060102150405"), deploy)
}

// 返回应用程序名称列表
func (c *Config) AppNames() []string {
	return SliceMapper(c.Apps, func(app *ConfigApp) (string, bool) { return app.Name, true })
}

// 查找 App 配置
func (c *Config) FindApp(name string) *ConfigApp {
	for _, app := range c.Apps {
		if app.Name == name {
			return app
		}
	}
	return nil
}

// 查找资源配置
func (c *Config) FindResource(name string) *ConfigResource {
	for _, r := range c.Resources {
		if r.Name == name {
			return r
		}
	}
	return nil
}

// 验证配置
func (c *Config) Validate() error {
	if c == nil {
		return NewErr(runtime.Caller(0)).Msg("config is nil")
	}
	c.Backup = filepath.Clean(c.Backup)
	if err := os.MkdirAll(c.Backup, 0755); err != nil {
		return NewErr(runtime.Caller(0)).Err(err).Msg("create backup directory error:")
	}
	for _, r := range c.Resources {
		if err := r.Validate(); err != nil {
			return NewErr(runtime.Caller(0)).Err(err).Msg("validate resource error")
		}
	}
	for _, a := range c.Apps {
		if err := a.Validate(); err != nil {
			return NewErr(runtime.Caller(0)).Err(err).Msg("validate app error:")
		}
	}
	for _, u := range c.Uploads {
		if err := u.Validate(); err != nil {
			return NewErr(runtime.Caller(0)).Err(err).Msg("validate upload error:")
		}
	}
	if c.Interval == nil {
		c.Interval = &ConfigInterval{Process: 10000, Upload: 30000, Upgrade: 60000}
	}
	if c.Interval.Process <= 100 {
		c.Interval.Process = 100
	}
	if c.Interval.Upload <= 100 {
		c.Interval.Upload = 100
	}
	if c.Interval.Upgrade <= 100 {
		c.Interval.Upgrade = 100
	}
	return nil
}

// 时间间隔
type ConfigInterval struct {
	Process int64 `yaml:"process"` // 检查进程的时间间隔（毫秒）
	Upload  int64 `yaml:"upload"`  // 上传文件的时间间隔（毫秒）
	Upgrade int64 `yaml:"upgrade"` // 检查更新的时间间隔（毫秒）
}

// 应用程序
type ConfigApp struct {
	Name   string          `yaml:"name"`   // 程序名称
	Work   string          `yaml:"work"`   // 程序的工作目录
	Deploy []*ConfigDeploy `yaml:"deploy"` // 应用部署
	// 应用的进程启动的命令行（第一个为启动命令，后续者为参数）。
	// 此字段用于监控进程是否存在。支持 ${ENV} 格式作为环境变量。
	Process []string `yaml:"process"`
	// 应用的启动命令，如果为空，则使用 Process 字段来启动进程。支持 ${ENV} 格式作为环境变量。
	// @TODO 待实现
	Start []string `yaml:"start"`
	// 应用的停止命令，如果为空，则使用 Kill 来停止进程。支持 ${ENV} 格式作为环境变量。
	// @TODO 待实现
	Stop []string `yaml:"stop"`
	// 应用的重载命令，重载是让应用重新加载配置，而不是重启进程。
	// 如果为空，不做任何动作。支持 ${ENV} 格式作为环境变量。
	// @TODO 待实现
	Reload []string `yaml:"reload"`
}

// 验证配置
func (c *ConfigApp) Validate() error {
	if c == nil {
		return NewErr(runtime.Caller(0)).Msg("config.apps[?] is nil")
	}
	c.Name = strings.TrimSpace(c.Name)
	if c.Name == "" {
		return NewErr(runtime.Caller(0)).Msg("config.apps[?].name is empty")
	}
	c.Work = filepath.Clean(c.Work)
	if c.Work == "" || c.Work == "." || c.Work == "./" || c.Work == `.\` {
		c.Work, _ = os.Executable()
		c.Work = filepath.Dir(c.Work)
	}
	if !filepath.IsAbs(c.Work) {
		return NewErr(runtime.Caller(0)).Msg("config.apps[%s].work is not absolute path", c.Name)
	}
	if err := os.MkdirAll(c.Work, 0755); err != nil {
		return NewErr(runtime.Caller(0)).Err(err).Msg("config.apps[%s].work mkdir:", c.Name)
	}
	if stat, err := os.Stat(c.Work); err != nil {
		return NewErr(runtime.Caller(0)).Err(err).Msg("config.apps[%s].work stat:", c.Name)
	} else if !stat.IsDir() {
		return NewErr(runtime.Caller(0)).Msg("config.apps[%s].work is not a directory", c.Name)
	}
	if len(c.Deploy) <= 0 {
		return NewErr(runtime.Caller(0)).Msg("config.apps[%s].deploy is empty", c.Name)
	}
	for _, d := range c.Deploy {
		if err := d.Validate(); err != nil {
			return NewErr(runtime.Caller(0)).Err(err)
		}
	}
	c.Process = SliceFilterEmptyFunc(c.Process, CleanCmdArg)
	if len(c.Process) <= 0 {
		return errors.New("config.apps[?].process is empty")
	}
	c.Start = SliceFilterEmptyFunc(c.Start, CleanCmdArg)
	c.Stop = SliceFilterEmptyFunc(c.Stop, CleanCmdArg)
	c.Reload = SliceFilterEmptyFunc(c.Reload, CleanCmdArg)
	return nil
}

// 查找部署配置
func (c *ConfigApp) FindDeployByPath(path string) *ConfigDeploy {
	for _, d := range c.Deploy {
		if d.Deploy == path {
			return d
		}
	}
	return nil
}

// 部署
type ConfigDeploy struct {
	Name     string `yaml:"name"`     // 部署名称，可以用于备份目录的一部分
	Resource string `yaml:"resource"` // 远程资源名称
	// 部署目录或文件。
	// 如果资源是单一的普通文件，则 Deploy 为部署的完整文件名（绝对路径）；
	// 如果资源是压缩包，则 Deploy 为部署的目录名（绝对路径），需要压缩包中哪些文件则由 Files 确定。
	Deploy string `yaml:"deploy"`
	// 当资源的压缩包时，指定哪些文件或目录需要部署到 Deploy 目录中。
	//
	// - 如果指定为空，或者设置为 ["*"]，则将整个压缩包部署到 Deploy 目录中；
	//
	// - 指定为 ["path/to/file", ...] 时，则会部署到 ${Deploy}/path/to/file, ... 目录
	//
	// 部署时，会按压缩包的目录结构进行部署；如果需要删除前缀，请配置 Prefix 字段；
	// 如果要打破此结构，则请使用多个 BootDeploy 节点，让 Resource 引用同一个资源来实现。
	// 例如用两个 BootDeploy 节点，Deploy 分别为 "/my/abc" 和 "/my/def"，
	// Files 分别为 ["path/to/file1"] 和 ["path/to/file2"]，Prefix 都为 path/
	// 则最终部署为：/my/abc/to/file1 和 /my/def/to/file2。
	Files []string `yaml:"files"`
	// 部署到 Deploy 目录时，删除压缩包中的路径前缀。
	// 比如压缩包中 path/to/file，设置为 path/ 时，将部署到 ${Deploy}/to/file。
	Prefix string `yaml:"prefix"`
}

// 验证配置
func (c *ConfigDeploy) Validate() error {
	if c == nil {
		return NewErr(runtime.Caller(0)).Msg("config.apps[?].deploy[?] is nil")
	}
	c.Name = strings.TrimSpace(c.Name)
	if c.Name == "" {
		return NewErr(runtime.Caller(0)).Msg("config.apps[?].deploy[?].name is empty")
	}
	c.Resource = strings.TrimSpace(c.Resource)
	if c.Resource == "" {
		return NewErr(runtime.Caller(0)).Msg("config.apps[?].deploy[%s].resource is empty", c.Name)
	}
	c.Deploy = filepath.Clean(c.Deploy)
	c.Prefix = CleanRelatedPath(c.Prefix)
	c.Files = SliceFilterEmptyFunc(c.Files, CleanRelatedPath)
	if len(c.Files) > 0 {
		if stat, err := os.Stat(c.Deploy); err == nil && !stat.IsDir() {
			msg := "when files are not empty, deploy must be a directory"
			return NewErr(runtime.Caller(0)).Msg("config.apps[?].deploy[%s].deploy is invalid: %s", c.Name, msg)
		}
		if err := os.MkdirAll(c.Deploy, 0755); err != nil {
			return NewErr(runtime.Caller(0)).Err(err).Msg("config.apps[?].deploy[%s].deploy mkdir error", c.Name)
		}
	} else {
		return NewErr(runtime.Caller(0)).If(os.MkdirAll(filepath.Dir(c.Deploy), 0755))
	}
	return nil
}

// 远程资源
type ConfigResource struct {
	Name string `yaml:"name"` // 文件名称
	// 资源类型，目前支持以下类型：
	//
	// - bin：二进制文件（默认），专指单一的普通文件。如 .exe、单个的 .json, .yaml 等配置文件
	//
	// - tgz：压缩包 tar.gz 文件
	//
	// - zip：压缩包 zip 文件
	Type string `yaml:"type"`
	Uri  string `yaml:"uri"` // 远程资源地址（URL）
}

func (c *ConfigResource) IsPackage() bool {
	return c.Type == "tgz" || c.Type == "zip"
}

// 验证配置
func (c *ConfigResource) Validate() error {
	if c == nil {
		return NewErr(runtime.Caller(0)).Msg("config.resources[?] is nil")
	}
	c.Name = strings.TrimSpace(c.Name)
	if c.Name == "" {
		return NewErr(runtime.Caller(0)).Msg("config.resources[?].name is empty")
	}
	c.Type = strings.ToLower(strings.TrimSpace(c.Type))
	switch c.Type {
	default:
		return NewErr(runtime.Caller(0)).Msg("config.resources[%s].type is invalid", c.Name)
	case "tgz":
		return NewErr(runtime.Caller(0)).Msg("config.resources[%s].type == tgz is unsupported now", c.Name)
	case "bin", "zip": // OK
	}
	if err := configCheckUrl(c.Uri); err != nil {
		return NewErr(runtime.Caller(0)).Err(err).Msg("config.resources[%s].uri parse error:", c.Name)
	}
	return nil
}

// 上传配置
type ConfigUpload struct {
	Local  string `yaml:"local"`  // 本地目录或文件（文件路径）
	Remote string `yaml:"remote"` // 远程目录或文件（URL）
	Append bool   `yaml:"append"` // 是否以追加方式上传，默认为 false
}

// 验证配置
func (c *ConfigUpload) Validate() error {
	if c == nil {
		return NewErr(runtime.Caller(0)).Msg("config.uploads[?] is nil")
	}
	c.Local = filepath.Clean(c.Local)
	c.Remote = strings.TrimRight(c.Remote, "/")
	if err := configCheckUrl(c.Remote); err != nil {
		return NewErr(runtime.Caller(0)).Err(err).Msg("config.uploads[?].remote parse error:")
	}
	return nil
}

// 验证配置中的 URL
func configCheckUrl(uri string) error {
	_, err := url.Parse(uri)
	return err
}

// 从文件中加载配置
func ConfigLoadFromFile(fn string) (*Config, error) {
	cf, err := os.Open(fn)
	if err != nil {
		return nil, err
	}
	defer cf.Close()
	cnf := &Config{}
	if err := yaml.NewDecoder(cf).Decode(cnf); err != nil {
		return nil, err
	}
	return cnf, nil
}

// 从 http 加载配置
func ConfigLoadFromHttp(uri string, header http.Header) (*Config, error) {
	req, err := http.NewRequest(http.MethodGet, uri, nil)
	if err != nil {
		return nil, err
	}
	for k, vs := range header {
		for _, v := range vs {
			req.Header.Add(k, v)
		}
	}
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusMultipleChoices {
		return nil, fmt.Errorf("load config fail: http status code: %d", res.StatusCode)
	}
	cnf := &Config{}
	if err := yaml.NewDecoder(res.Body).Decode(cnf); err != nil {
		return nil, err
	}
	return cnf, nil
}
